Skip to content

[pull] develop from duckduckgo:develop#350

Open
pull[bot] wants to merge 5667 commits intoRachelmorrell:developfrom
duckduckgo:develop
Open

[pull] develop from duckduckgo:develop#350
pull[bot] wants to merge 5667 commits intoRachelmorrell:developfrom
duckduckgo:develop

Conversation

@pull
Copy link

@pull pull bot commented Aug 19, 2021

See Commits and Changes for more details.


Created by pull[bot]

Can you help keep this open source service alive? 💖 Please sponsor : )

YoussefKeyrouz and others added 22 commits February 9, 2026 14:10
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1213130409431278

### Description

First part of the recent chats feature, which implements a chat
suggestion list in the duck.ai tab of the input screen.

Introduces a new feature flag: aiChatSuggestions. When enabled, the AI
chat suggestions (pinned and recent chats) in the input screen's duck.ai
mode.

The UI is currently a work in progress and is using static test data.

*Changes:*

- Added a new overlay in the input screen fragment for the chat
suggestions. This follows the same pattern as the autocomplete overlay.
- Add a recycler view (with its adapter and items) to the
chatSuggestionsOverlay to display the list of recent chats.
- Modified how the Logo visibility is handled. The Duck.ai chat logo
should no longer be visible even if there are not recent chat items.
Only the search logo will be visible (Follows iOS implementation)
- Added icons for the chats (pin vs chat bubble)
- Added the model for the Chat suggestion. It follow the expected
structure for future integration with the JS frontend.
- Modified the Input Screen View Model to load the static list.
- All the changes are UX/fragments, Testing will be done through
Maestro. Unit tests will be implemented when real data starts flowing
through the view model.


*Next Steps*
- Connect with the real frontend data once the JS webview interface is
ready
- Clicking on a recent chat should navigate you to the Duck.ai page and
load the related chat.
- Maestro tests

### Steps to test this PR
Notes: in order to facilitate testing, I created an additional branch
with static data. Please pull the
`origin/feature/youssef/do_not_merge/recent_ai_chats_ui_test_data`
branch to test this PR with data and see the behavior. This would
prevent having test data part of the merge and allow PR testing.

- Pull the changes and run the app
- Go to feature flags settings and turn on "aiChatSuggestions" flag
(it's off by default)
- Go back to the input screen, make sure the omnibar is enabled
- Switch to the "Duck.ai" tab. You should see a static list of chats,
both pinned and recent.
- Switch back and forth between search and chat to make sure the
interaction and visibility is correct. You can test different behaviors
on the search tab such as searching or adding a favorite. No impact
should be observed.
- If "aiChatSuggestions" is disabled, the app should behave as it is
currently in production. No impact should be observed

### UI changes
| Before  | After |
| ------ | ----- |

|![before](https://github.com/user-attachments/assets/345cd1b6-4b19-4a61-9a4d-704335429632)|![after](https://github.com/user-attachments/assets/edf33a8c-c99e-4a2a-83c5-bbf4fee837fe)|

||![after](https://github.com/user-attachments/assets/f774569e-bf60-4e91-b140-2c37f2dd3e7e)|


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Feature-flagged but touches core input-screen UI state/animation and
overlay interaction, which can introduce subtle regressions in tab
switching, visibility, and touch handling.
> 
> **Overview**
> Adds a new remote sub-feature flag,
`DuckChatFeature.aiChatSuggestions`, to gate an in-progress
“recent/pinned chats” suggestions experience in the Duck.ai input-screen
tab.
> 
> When enabled, the input screen now includes a new
`chatSuggestionsOverlay` (RecyclerView + bottom fade/blur) and shared
overlay animation logic, and updates logo/overlay/viewpager-interaction
behavior to avoid showing the Duck.ai logo and to ensure
autocomplete/suggestions overlays don’t overlap during mode switches.
`ChatTabFragment` is expanded to wire up a `ChatSuggestionsAdapter` and
observe a new `InputScreenViewModel.chatSuggestions` flow (loading
stubbed via a TODO), with new pin/chat icons and an
`item_chat_suggestion` row layout; tests are updated to inject the new
feature toggle dependency.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
595d297. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Youssef Keyrouz <ykeyrouz@Youssefs-MacBook-Pro.local>
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1213085863198314
### Description
See attached task description

### Steps to test this PR
https://app.asana.com/1/137249556945/task/1213176315984932

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches analytics/pixel APIs and their call sites, requiring signature
updates across production and tests; runtime risk is mitigated by
wrapping VPN state lookup in `runCatching` and defaulting to `false`.
> 
> **Overview**
> PIR now enriches eligible scan/opt-out result pixels with a new
`vpn_connection_state` parameter (mapped to `connected`/`disconnected`)
for `PIR_SCAN_STAGE_RESULT_*` and opt-out submit success/failure pixels.
> 
> `RealPirRunStateHandler` is updated to depend on
`NetworkProtectionState` and safely query `isRunning()` (fail-closed)
when emitting these pixels; tests and the end-to-end instrumentation
setup add a `FakeNetworkProtectionState`, and `pir-impl` now depends on
`:network-protection-api`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
754362f. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
…icons. (#7710)

Task/Issue URL:
https://app.asana.com/1/137249556945/task/1212341818869426

### Description
Handle unreasonably large favicons gracefully, by setting a max size in
pixels.

### Steps to test this PR
        
1. Start a local HTTP server serving a page with a large
apple-touch-icon:
cd /tmp/favicon_test && python3 -m http.server 8888 (The test page
serves a 6708×6708 PNG as its touch icon)
2. On the device, open the DuckDuckGo browser and navigate to
http://{your-server-ip}:8888
  3. Wait ~2 seconds for the touch icon to be discovered and downloaded
  4. Open the tab switcher
  5. Without the changes: App crashes with:
`RuntimeException: Canvas: trying to draw too large(179989056bytes)
bitmap` But with the changes shouldn't crash.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Small, localized change that only constrains favicon bitmap
dimensions; low risk aside from potential quality reduction for very
large icons.
> 
> **Overview**
> Prevents crashes from *unreasonably large* favicons by introducing a
`MAX_FAVICON_SIZE_PX` cap (512px) and enforcing it across Glide favicon
fetches.
> 
> Both async (`CustomTarget`) and sync (`submit`) download paths for
disk and URL loads now request bitmaps at the capped size, limiting
memory usage during decoding/rendering.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
91ca909. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1213076149941930

### Description
Fixes `Room` schema export by switching to the Room Gradle plugin.
Several modules were using `annotationProcessorOptions` to configure the
schema location, but this only works with `kapt`, not `KSP`. Since
`Room` is compiled with `KSP` in these modules, schemas weren't being
exported.

Additionally (the original reason I was looking at this), absolute paths
were being used which breaks `Gradle` caching.

 **Changes**

  - Added `androidx.room Gradle` plugin to affected modules
- Replaced `annotationProcessorOptions` and `ksp { arg(...) }` with the
new room `{ schemaDirectory(...) }`

  **Why the JSON schema changes/creation**
Because many of the DB schema files were missing, and one was wrong.
They've been missing for some time.

  6 databases were affected:
  | Database                | Missing Schemas     |
  |-------------------------|---------------------|
  | AppDatabase             | versions 50-60      |
  | VpnDatabase             | versions 33-34      |
  | RemoteMessagingDatabase | version 2           |
  | SitePermissionsDatabase | versions 2-5        |
  | VoiceSearchDatabase     | version 2           |
  | BrokenSiteDatabase      | incorrect v1 schema |

2 databases were already working (`PirDatabase`, `Autofill` databases)
because they used the correct `ksp { arg(...) }` syntax already.

**Why this matters**

- Enables Gradle build caching (absolute paths in old config broke cache
keys)
  - Schema JSON files are now correctly exported for migration testing
  - Uses the officially recommended Room configuration method

### Steps to test this PR
- Fresh install develop, do some stuff in the app then install this
branch. Verify no problems in the upgrade

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Build config changes span many modules and regenerate schema
artifacts; risk is mainly in build/cache behavior and Room migration
test expectations rather than runtime logic.
> 
> **Overview**
> **Migrates Room schema export configuration to the official
`androidx.room` Gradle plugin** across multiple modules (including
`app`, `vpn-store`, `broken-site-store`, `remote-messaging-store`,
`site-permissions-store`, `voice-search-store`, `experiments-impl`,
`pir-impl`, and `autofill-impl`). This removes the old
`annotationProcessorOptions`/`ksp { arg("room.schemaLocation", ...) }`
approach, switches schema paths to project-relative
(`$projectDir/schemas`), and adds `room { schemaDirectory(...) }` plus
schema assets wiring for tests.
> 
> Adds/updates Room exported schema JSONs for several databases (new
versions for `AppDatabase`, `VpnDatabase`, `RemoteMessagingDatabase`,
`SitePermissionsDatabase`, `VoiceSearchDatabase`) and corrects
`BrokenSiteDatabase` v1 schema (primary key/identity hash), enabling
migration testing and improving Gradle build cache reliability.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
6b0827e. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Craig Russell <1336281+CDRussell@users.noreply.github.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1201870266890790/task/1210936487262413?focus=true

### Description

Our other browsers handle these states similarly for the most part, so
we should adapt Android to match other platforms. As it stands, we get
breakage reports from Android users whenever the privacy shield is
showing the red warning dot, which means we get a lot of non-actionable
feedback for people's router IPs and for sites we're trying to
speculatively mitigate breakage on (turning off blocking to see if it
reduces reports).

### Steps to test this PR
- [x] Navigate to `noaprints.com` and `marvel.com` (both have protection
disabled in the config)
- [x] Confirm for each that you see the normal green privacy shield
state for both (though if you click into the dashboard, the fact that
protections are disabled is shown & the menu item shows as "enable
privacy protection"
- [x] Navigate to your local network address(es) and confirm that you
see the globe icon rather than the UNPROTECTED state (privacy shield
with a red dot)

### UI changes
| Before  | After |
| ------ | ----- |
<img width="1080" height="2400" alt="marvelOld"
src="https://github.com/user-attachments/assets/695101f5-1ac0-49d0-a885-fd9efef852f8"
/>|<img width="1080" height="2400" alt="marvelNew"
src="https://github.com/user-attachments/assets/cb880287-3e5a-4350-8c92-cce61469dd60"
/>|
|<img width="1080" height="2400" alt="localOld"
src="https://github.com/user-attachments/assets/f1a8126b-8222-4bac-b23a-6a62afc65eb8"
/>|<img width="1080" height="2400" alt="localNew"
src="https://github.com/user-attachments/assets/0179bc49-8096-4908-bf1d-4813af3039c2"
/>|




<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes how privacy/shield and leading icon states are derived
(user-visible and used by reporting), gated by a remote toggle but
touching core URL classification and privacy-state logic.
> 
> **Overview**
> Standardizes omnibar leading icon/shield behavior behind a new remote
feature toggle `standardizedLeadingIcon`.
> 
> When enabled, `OmnibarLayoutViewModel` shows the **Globe** icon for
localhost/private-network/file URLs (instead of the privacy shield
state), and `SiteMonitor.privacyProtection()` only returns
**UNPROTECTED** for *user-initiated* allowlisting (not remote-config
exceptions), reducing misleading red-dot states. Adds a new
`Uri.isLocalUrl` helper (IPv4/IPv6 ranges + localhost, no DNS lookups)
with extensive tests, and updates constructors/tests to inject the new
toggle through `SiteFactoryImpl`/`SiteMonitor`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
d5bc522. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Kate Manning <laghee@users.noreply.github.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1213210525832193
### Description

On the input screen view, we are able to switch tabs (between Search and
Duck.ai) using the swipe gesture. When we type something in the search
box the autocomplete overlay is shown and a list of autocomplete
suggestions is displayed. Swipe gesture works inside the list items but
not in empty areas.

### Steps to reproduce 
- Go to Settings → AI Features → Make sure “Duck.ai” is turned on and
“Search & Duck.ai” mode is selected.
- Go back to the Input screen with the omnibar. The Search tab is
selected by default
- Swipe to the left anywhere on the page → Tab switches to Duck.ai tab
correctly
- Go back to the Search and type something in the chat box for the
autocomplete list to show up
- Swipe to the left inside the list → Tab switches correctly
- Swipe to the left somewhere outside the list (in any empty area) → Tab
does NOT switch. Swipe gesture is ignored.

### Root cause and fix
When touching a list item, `onInterceptTouchEvent` receives ACTION_MOVE
events (to allow the parent to steal it from the child). This is where
horizontal swipe detection is currently implemented.
When touching empty space below the items, no child handles ACTION_DOWN,
so Android skips onInterceptTouchEvent for subsequent MOVE events and
routes them directly to the recycler view's own onTouchEvent.
onInterceptTouchEvent is never called for the ACTION_MOVE when no list
item claimed the ACTION_DOWN.
   
Fix: Added horizontal swipe detection in the recycler view's
onTouchEvent to cover this path. If it's a move action, and the
interceptor didn't already detect it, we detect it here.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Small, localized touch-handling change in a single custom
`RecyclerView`; risk is limited to potential gesture-detection
regressions in this view.
> 
> **Overview**
> Fixes swipe-to-switch-tabs when interacting with empty space in the
autocomplete/chat suggestions overlays by adding horizontal-swipe
detection to `SwipeableRecyclerView.onTouchEvent` (not just
`onInterceptTouchEvent`).
> 
> This ensures `ViewPager2` receives the gesture even when `ACTION_MOVE`
events bypass interception, improving tab switching consistency during
overlay display.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
e425554. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Youssef Keyrouz <ykeyrouz@Youssefs-MacBook-Pro.local>
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1212863124420545

### Description
Adding my Asana ID to the github Asana mapping for automated task
assignment to work.


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Single-line update to a GitHub-to-Asana ID lookup table used by
automation; no code or security-sensitive logic changes.
> 
> **Overview**
> Adds `YoussefKeyrouz` to
`.github/actions/assign-release-task/github_asana_mapping.yml` so the
`assign-release-task` GitHub Action can map that GitHub username to the
correct Asana user ID for automated task assignment.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
192875c. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Youssef Keyrouz <ykeyrouz@Youssefs-MacBook-Pro.local>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/488551667048375/task/1213011616457504?focus=true

### Description
Replace pillIcon images in ListItem components for the new custom view
where we can set text as string

### Steps to test this PR

- [ ] Install from branch
- [ ] Go to Settings and check everything looks good
- [ ] Go to Android Design System Preview > List Items
- [ ] Check all the yellow pills there look ok

### No UI changes

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches widely used design-system list-item views and their public XML
attributes; risk is mainly UI regressions and attribute migration issues
where old `showBetaPill`/`showNewPill`/enum `pillIcon` were referenced.
> 
> **Overview**
> **Replaces static “Beta/New” pill image usage in list-item components
with the `DaxYellowPill` text-based custom view.** `OneLineListItem`,
`TwoLineListItem`, and `SettingsListItem` now show/hide a `yellowPill`
and set its label via new `pillText` when `pillIcon` is enabled,
removing the old `showBetaPill`/`showNewPill` flags and the enum-based
`pillIcon` resource mapping.
> 
> Updates the associated XML layouts and design-system preview
components to use `DaxYellowPill` and the new attrs, and removes the
now-unused `showBetaPill` attribute from `WaitlistCheckListItem` attrs.
Also tweaks constraints/minHeight in item layouts to accommodate the new
pill view.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
068d064. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1213119876946268

### Description
This PR adds translations for the Duck.ai Contextual work

### Steps to test this PR
Green CI

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Changes are limited to Android string resources and a small wiring
update to reference the new summarize string key, with minimal
functional impact beyond displayed text.
> 
> **Overview**
> Adds localized `Duck.ai` contextual bottom-sheet strings (e.g.,
summarize prompt and page-content attachment/auto-send labels) across
many `values-*/strings-duckchat.xml` locales and the default
`values/strings-duckchat.xml`.
> 
> Updates `DuckChatContextualFragment` to use the new
`duckAIContextualPromptSummarize` string resource (replacing the
previous `duckAIContextualSummarizePrompt`) and removes the old
`values/donottranslate.xml` resource file; also includes minor test
renaming and small SERP logo string metadata/text tweaks.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
cc4ac8e. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1213119876946268

---------

Co-authored-by: Dax The Translator <daxmobile@duckduckgo.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1213081921435007

### Description
This PR improves the prompt replacement logic

### Steps to test this PR
Enable contextualMode FF

_No previous prompt_
- [x] Open contextual and tap on “Summarise This Page"
- [x] Verify prompt has been replaced (no spaces before or after)

_With previous prompt_
- [x] Open contextual and enter some text in the input field
- [x] Tap on “Summarise This Page"
- [x] Verify prompt has been added at the end of the current prompt

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Changes are limited to contextual prompt UI/state handling and
add/adjust unit tests; no auth, networking, or persistence logic is
materially altered.
> 
> **Overview**
> Improves Duck.ai contextual *auto-prompt* behavior by changing
`replacePrompt` to append the predefined “Summarize” prompt to any
existing user input (or replace it when empty), and to only show page
context when the cached context JSON is valid.
> 
> Moves prompt clearing to the ViewModel via a new `onPromptCleared` (so
clearing doesn’t implicitly toggle context), updates the fragment to
keep the cursor at the end when restoring a prompt, and expands/updates
`DuckChatContextualViewModelTest` coverage for these cases.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
66a629c. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1212015278241917/task/1212011586318109?focus=true

### Description

Display the about:blank title for about blank tab

### Steps to test this PR

_Check about:blank tab name_
- [x] Open the application
- [x] Type "about:blank" in the address bar and submit
- [x] Check your page is blank
- [x] Go to tab screen
- [x] Check the title of the tab is empty (not covered by this task)
- [x] Open a new tab and enter a website with its own favicon
- [x] Go to tab screen
- [x] Check the title of the new tab, should have a title corresponding
to the website
- [x] Open a new Duck.ai tab
- [x] Go to tab screen
- [x] Check the title of the tab "Duck.ai" with Dax as favicon 
- [x] Open
`http://privacy-test-pages.site/security/address-bar-spoofing/spoof-about-blank-emptyaddress.html`
- [x] Click on "Run" button
- [x] Check a blank page has been opened
- [x] Go to tab screen
- [x] Check the title of the tab `about:blank` and the icon is a globe

### UI changes
| Before  | After |
| ------ | ----- |
<img width="270" height="600" alt="image"
src="https://github.com/user-attachments/assets/9fd438d8-adb6-49ad-a70b-6c6da32ecc54"
/> | <img width="270" height="600" alt="image"
src="https://github.com/user-attachments/assets/5b401c61-bc59-4e2a-a181-1915ac46ff8f"
/>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1210856607616307/task/1213179366895625

### Description
Fix pixel definition

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Schema-only change to an analytics/pixel definition with minimal
functional impact; main risk is downstream validation/consumers
expecting the old shape.
> 
> **Overview**
> Updates the
`m_appearance_settings_is_tracker_count_in_address_bar_toggled` pixel
definition in `appearance_settings.json5` to include a new `parameters`
entry, `is_enabled`, capturing whether the setting is enabled or
disabled after the toggle.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
98ccc9d. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1212608036467427/task/1212999357864763?focus=true

### Description
Introduce a feature flag to control whether we disable the tracker
animation upon launching the browser. This works by setting the
prevoiusUrl to the current url being loaded, which would effectively
stop the animation from playing.

An edge case of launching the app from an external link should be
handled via the isExternal flag already existing in the viewModel.
However, there was a race-condition in how the `isExternal` flag was
being
[evaluated](https://github.com/duckduckgo/Android/blob/df6e0a43589aed21d44cabbbc9e8a0341185a73e/app/src/main/java/com/duckduckgo/app/browser/tabs/adapter/TabPagerAdapter.kt#L72).
This is problematic on 2 different dimensions: 1- Tab loading order will
affect which tab is reading isExternal from the latest intent. 2-
Reading wrong intent as sometimes the correct intent to read is a
deferred intent stored in
[lastIntent](https://github.com/duckduckgo/Android/blob/df6e0a43589aed21d44cabbbc9e8a0341185a73e/app/src/main/java/com/duckduckgo/app/browser/BrowserActivity.kt#L478).

I made minimal changes to fix how the isExternal is being read by
storing the intended value for isExternal in a <tabId to isExternal
map>, and only reading that once upon creation of the fragment.

The reason why this shouldn't be in the tabRepository is that isExternal
should only be used upon creation of the tab for the first time only. If
for example the user relaunches the app, isExternal for an old tab
should be set to false.

### Steps to test this PR
- Open app
- Enter a new website.
- observer tracker animation.
- close app and make sure process is killed.
- start app.
- observe same website loading but without tracker animation.




### UI changes
| Before  | After |
| ------ | ----- |

![before](https://github.com/user-attachments/assets/87f50dca-9cc5-4300-95d0-c9eb0505a50c)|![after](https://github.com/user-attachments/assets/0c34dde9-30f1-4ea5-b3d1-4b9b250bc179)




<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches tab creation/selection flow and external-intent handling,
which can affect navigation behavior and first-load experience across
multiple tabs. Changes are scoped and feature-flagged, but regressions
could show up as incorrect external-tab treatment or missing/extra
animations.
> 
> **Overview**
> Adds a new remote-config toggle `disableTrackerAnimationOnRestart` and
a one-shot `suppressTrackerAnimation` path that, on app restart, sets
`BrowserTabViewModel.previousUrl` to the initial URL to prevent the
tracker animation from playing.
> 
> Refactors how *external launches* are determined by tracking external
status per `tabId` in `BrowserActivity` and consuming it when
creating/selecting tab fragments, avoiding races from reading the latest
`Intent`. Updates fragment/tab creation APIs to pass
`suppressTrackerAnimation`, adds debug logging, and extends
`BrowserTabViewModelTest` to cover the new `previousUrl` behavior and
guardrails (feature flag, external tabs, null URL).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
2557255. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1213218457009900

### Description
Add platform parameter to VPN activation pixel

### Steps to test this PR
- [x] Apply patch on
https://app.asana.com/1/137249556945/task/1210448620621729
- [x] Fresh install
- [x] Purchase a test subscription (Free Trial)
- [x] Before it expires activate VPN
- [x] Check in logcat that `subscription_free_trial_vpn_activation`
pixel is fired with the new `platform` parameter
### No UI changes

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Small, localized analytics/pixel schema change; main risk is
missing/incorrect `platform` values causing pixel schema mismatches or
downstream reporting issues.
> 
> **Overview**
> Adds a new `activation_platform` parameter to the
`subscription_free_trial_vpn_activation_u` pixel definition (enum:
`apple`/`google`/`stripe`).
> 
> Updates `SubscriptionPixelSender.reportFreeTrialVpnActivation` to
require and send this platform value, and wires
`FreeTrialConversionWideEvent` to pass `subscription.platform`;
associated unit tests are updated to assert the new argument and
non-invocation signature.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
0d48361. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1213074060364697

### Description
This PR ensures that when attaching context in the FE side we are
properly showing the favicon

### Steps to test this PR
Enable contextualMode

_Feature_
- [x] Open app and open a contextual chat
- [x] Send a prompt, but don’t attach the context
- [x] When in Duck.ai, tap on Attach Page Content
- [x] Verify context is added, and favicon too.

### UI changes
| Before  | After |
| ------ | ----- |
<img width="1080" height="2400" alt="Screenshot_20260127_202941"
src="https://github.com/user-attachments/assets/18b6f145-68cf-4996-bd75-aedaa28ce090"
/>|<img width="1080" height="2424" alt="Screenshot_20260209_224902"
src="https://github.com/user-attachments/assets/8903ddd2-7179-4a6e-bbca-7503982f6194"
/>|



<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches JS-bridge payload generation and adds bitmap-to-base64
encoding plus disk favicon lookup, which could affect performance/memory
and the shape of data sent to the webview.
> 
> **Overview**
> Duck.ai contextual mode now passes the current `tabId` through JS
callback handling and, when responding to `getAIChatPageContext`, looks
up the tab/url favicon on disk and injects it into the returned
`pageContext` as a `favicon` array containing a
`data:image/png;base64,...` icon.
> 
> This updates the contextual sheet to store/forward `sheetTabId`,
extends `DuckChatJSHelper.processJsCallbackMessage` with a `tabId`
parameter, and adjusts unit/instrumentation tests to cover the new
argument and favicon enrichment behavior.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
76bd669. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1213074060364697
…in state (#7722)

Task/Issue URL:
https://app.asana.com/1/137249556945/task/1213228272275032

### Description
Updates SetUpSyncHandler validation so the "sync already on" error is
only returned for sendToSetupSync; sendToSyncSettings now proceeds to
open Sync settings regardless of sign-in state.

### Steps to test this PR
- QA optional
- [ ] Enable internal duck ai chat state: [Internal Testing Chat Sync
Integration with Native
Apps](https://app.asana.com/1/137249556945/task/1212472824864986?focus=true)
- [ ] Enable sync
- [ ] Visit duck ai chat settings and click on the `Manage` button under
`Sync & Backup`; verify sync settings activity is launched

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Small, well-tested change to JS message validation/gating that only
affects whether the Sync settings activity is launched.
> 
> **Overview**
> Updates `SetUpSyncHandler` validation so the **"sync already on"**
error is only returned for `sendToSetupSync`; `sendToSyncSettings` now
proceeds to open Sync settings regardless of sign-in state.
> 
> Refactors method names into constants and expands tests to cover the
new behavior (error when feature disabled for both methods, and activity
launch for `sendToSyncSettings` when already signed in).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
cafd87e. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Craig Russell <1336281+CDRussell@users.noreply.github.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1213226424542893

### Description
Update incorrect string in PIR initial scan started notification

### Steps to test this PR
QA_optional

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> String-only UI copy change with no functional or data-handling impact;
risk is limited to messaging/consistency.
> 
> **Overview**
> Updates the PIR foreground scan notification copy by changing
`pirNotificationMessageInProgress` from “A scan is currently in
progress.” to “Your scan is in progress…”, improving feedback when an
initial scan starts.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
29c1d78. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1213213117376953

### Description

This PR adds a new destructive secondary button type.

### Steps to test this PR

- [ ] Go to Settings -> Android Design System Preview
- [ ] Tap on the Buttons tap and scroll down
- [ ] Verify the new secondary destructive button type is visible and
looks as expected

### UI changes
| Light  | Dark |
| ------ | ----- |

![Screenshot_20260210_185056.png](https://app.graphite.com/user-attachments/assets/82a8688c-fc8f-47dd-8ab6-db56de08de53.png)|![Screenshot_20260210_185111.png](https://app.graphite.com/user-attachments/assets/999d4ea5-e2d0-4e63-a9d5-3ec8313fc8e2.png)|


---
- To see the specific tasks where the Asana app for GitHub is being
used, see below:
  - https://app.asana.com/0/0/1213213117376953
  - https://app.asana.com/0/0/1213213535164007

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk, mostly additive UI/theming resources and a new `DaxButton`
subclass plus wiring in previews. Main risk is minor visual regressions
or lint rule false positives/negatives due to the updated
`DaxButtonStylingDetector` class list.
> 
> **Overview**
> Adds a new **destructive secondary** button variant to the design
system: a `DaxButtonDestructiveSecondary` view, a new
`ButtonType.DESTRUCTIVE_SECONDARY`, and theme/attribute wiring
(`daxButtonDestructiveSecondary`) with dedicated stroke/text/ripple
selectors and widget style.
> 
> Updates the design-system preview layout to showcase the new button in
small/large, icon, and disabled states, and extends the custom lint rule
(`DaxButtonStylingDetector`) plus its tests to recognize the new button
class (and updated `com.duckduckgo.common.ui.view.button.*` class
names).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
1371e45. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/task/1213210377163129

### Description
* Don't try to open context menu until bookmark added botomSheet has disappeared 

### Steps to test this PR

_Feature 1_
- [ ] [Release tests](https://app.maestro.dev/project/proj_01htg54rdtfwx8rgbzv03cxkpf/maestro-test/app/app_01hkqhj1thevwtn9ym8a2ctn2r/upload/mupload_01kh6eembtfm6vpmdhhfptas7c?sort=name) are passing on Maestro

### UI changes
n/a

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Test-only timing change; no production code or data flows are modified, with minimal risk beyond potentially masking a real UI hang if the prompt never dismisses.
> 
> **Overview**
> Stabilizes the Maestro release tests for adding/removing favorites from bookmarks by inserting an `extendedWaitUntil` step after tapping **add bookmark**.
> 
> Both `favorites_bookmarks_add.yaml` and `favorites_bookmarks_delete.yaml` now wait up to 5s for the "Bookmark added" bottom-sheet/prompt to disappear before reopening the menu and continuing, reducing flakiness from UI timing/race conditions.
> 
> <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 45dd0d9. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1212358379355435

### Description
This PR adds pixels to all agreed entry points

### Steps to test this PR
See task

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Primarily adds telemetry events and minor method renames; risk is
limited to potentially incorrect/duplicated pixel firing or slightly
altered context-validation behavior.
> 
> **Overview**
> Adds **new Duck.ai Contextual-mode telemetry** definitions and wiring,
introducing count + daily pixels for contextual sheet
open/dismiss/expand, session restoration, new-chat, summarize quick
action, page-context placeholder shown/tapped, page-context
attach/remove (native + frontend), prompt submission with/without
context, and invalid/empty context collection.
> 
> Hooks these pixels into the contextual UI flow
(`DuckChatContextualViewModel`/`Fragment`) and JS bridge
(`RealDuckChatJSHelper` adds `togglePageContextTelemetry`), and extends
daily reporting to include the automatic page-context setting state plus
firing enable/disable pixels when the setting is toggled. Tests are
updated/added to verify the new pixel emissions and the expanded allowed
JS method list.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
c32b5b8. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1213218785316169

### Description

This PR fixes the issue where sending a pixel shortly after enabling VPN
may fail.

### Steps to test this PR

- [x] Apply patch on
https://app.asana.com/1/137249556945/task/1210448620621729
- [x] Fresh install
- [x] Purchase a test subscription (Free Trial)
- [x] Before it expires activate VPN
- [x] Check in logcat that `subscription_free_trial_vpn_activation`
pixel is fired with the new `platform` parameter

### No UI changes

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Small, localized change to pixel delivery behavior; risk is limited to
analytics event timing/delivery for pixels using the new `enqueue` flag.
> 
> **Overview**
> Adds an `enqueue` flag to `SubscriptionPixel` and updates
`SubscriptionPixelSenderImpl.fire` to *conditionally* send pixels via
`pixelSender.enqueueFire` instead of immediate `fire`.
> 
> Marks `FREE_TRIAL_VPN_ACTIVATION` to use the queued send path,
reducing the chance the pixel is lost when VPN is enabled and network
conditions are transient.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
daeb405. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
catalinradoiu and others added 30 commits March 13, 2026 11:10
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1211724162604201/task/1211938369145727?focus=true

### Description
This PR adds the integration test for the request blocklist. The test
runs on
https://privacy-test-pages.site/privacy-protections/request-blocklist/
and checks that all the necessary requests are blocked/ loaded.

### Steps to test this PR
- [x] CI passes
- [x] RequestBlocklistTest passes

### UI changes
None.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> <sup>[Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) is
generating a summary for commit
04e9941. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1212827841082637

### Description

Adds support for adwall (aggregate) counting pixels via web telemetry.

### Steps to test this PR

Testing steps are covered in
https://github.com/duckduckgo/ddg-workflow/blob/gd-detector-telemetry/technical-designs/web-detection-framework/eventhub-android-testing-plan.md

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Adds a new backgrounded/foregrounded lifecycle-driven telemetry
pipeline that persists state in Room and fires pixels on timers, which
could impact analytics correctness and scheduling behavior. The feature
is gated by a remote toggle but touches WebView messaging and app
lifecycle hooks.
> 
> **Overview**
> Introduces a new **`eventHub` telemetry feature** that ingests
`webEvents`/`webEvent` messages from Content Scope Scripts, aggregates
counter-based metrics over configurable periods, and fires bucketed
pixels with an `attributionPeriod` parameter.
> 
> Adds a new `event-hub-impl` module with remote-config parsing,
per-pixel state persistence (Room), deduping per `webViewId`/navigation,
scheduling to fire at period end, and plugins/hooks for privacy-config
updates and app lifecycle foreground/background transitions. Updates
content-scope messaging to inject `nativeData.webViewId` into `webEvent`
messages, wires the module into `app/build.gradle`, and adds pixel
definitions for daily/weekly adwall detection telemetry.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
d880c78. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
…ixes (#7872)

Task/Issue URL: https://app.asana.com/1/137249556945/project/1198194956794324/task/1213548840330684?focus=true

### Description

Three fixes to `GlobalActivityStarter`:

**1. `startForResult()` overloads**

The previous pattern for launching an activity for result was `startIntent() + launcher.launch(intent)`. `startIntent()` returns `Intent?` — passing `null` to `launcher.launch()` crashes at runtime with no clear error. The new `startForResult(context, params, launcher)` overloads resolve this and make the intent clearer at the call site.

Before:
```kotlin
val intent = globalActivityStarter.startIntent(this, SomeActivityParams)
someLauncher.launch(intent) // crashes if intent is null
```

After:
```kotlin
globalActivityStarter.startForResult(this, SomeActivityParams, someLauncher)
```

**2. Automatic `FLAG_ACTIVITY_NEW_TASK` for non-Activity contexts**

`startIntent()` and `start()` now automatically add `FLAG_ACTIVITY_NEW_TASK` when `context !is Activity`. Callers in Services, broadcast receivers, and JS message handlers no longer need to add the flag manually after calling `startIntent()`.

Before:
```kotlin
val intent = globalActivityStarter.startIntent(context, DuckChatNativeSettingsNoParams)
intent?.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent)
```

After:
```kotlin
globalActivityStarter.start(context, DuckChatNativeSettingsNoParams)
```

**3. Defensive logging**

- `logcat(ERROR)` is emitted before `IllegalArgumentException` when no mapper is found for a params type, making registration bugs visible in logcat before the crash.
- `logcat(WARN)` is emitted when multiple mappers claim the same params type (previously the first match silently won with no diagnostic output).

Updated callers: `RestoreSubscriptionActivity` (migrated to `startForResult`), `OpenNativeSettingsHandler` (removed manual flag), `BookmarksActivity`/`BookmarksViewModel` (removed unused `LaunchSyncSettings` + `syncActivityLauncher` — the result callback only re-ran promotion eligibility, which already happens via other paths).

### Steps to test this PR

- [ ] Launch an activity for result using `startForResult()` — confirm it launches correctly and the result callback fires
- [ ] Restore a subscription via **Settings → Subscription → Restore** — confirm the restore flow launches and completes without a crash
- [ ] Open a Duck.ai chat, trigger the native settings from the SERP settings JS handler — confirm it opens without a crash and without needing `FLAG_ACTIVITY_NEW_TASK` manually
- [ ] Confirm no regression in Bookmarks — open bookmarks, verify sync promotion behaviour is unchanged
- [ ] Run `./gradlew :navigation-impl:testDebugUnitTest` — all tests should pass

---

> [!NOTE]
> **Medium Risk**
> Touches shared navigation infrastructure used across modules; intent flagging and mapper selection changes could affect activity launches if edge cases exist, though covered by new unit tests.
> 
> **Overview**
> Improves `GlobalActivityStarter` to be safer and more ergonomic: adds `startForResult(...)` overloads for `ActivityResultLauncher`, and centralizes intent construction with clearer error logging when no mapper is found.
> 
> `start()`/`startIntent()` now automatically apply `FLAG_ACTIVITY_NEW_TASK` for non-`Activity` contexts, and log a warning when multiple mappers match the same params (first match still wins). Call sites are updated to use the new APIs (e.g. subscriptions restore flow and SERP native-settings handler), and bookmarks removes a now-unused sync-settings launch path.
> 
> <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 8d5ce61. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/72649045549333/task/1213377943508982?focus=true

### Description
Updates privacy pro references inside PIR

### Steps to test this PR
N/A

### UI changes
| Before  | After |
| ------ | ----- |
!(Upload before screenshot)|(Upload after screenshot)|


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> String-only changes across many locales, plus an Android manifest
label tweak for `PirActivity`; risk is limited to UI text/regional
resource correctness and potential missing/unused string references.
> 
> **Overview**
> Updates localized UI copy to shift messaging from *Privacy Pro* to
*DuckDuckGo Subscription* in the Personal Information Removal (PIR)
flow, including changing `PirActivity`’s manifest label to
`@string/ddg_subscription`.
> 
> Adds new (currently English, `translatable="false"`) onboarding and
Duck.ai input-screen strings across multiple locales (e.g.,
Duck.ai-specific pre-onboarding titles/buttons, comparison-chart item,
and updated “search vs Duck.ai” preference labels).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
764f910. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Dax The Translator <daxmobile@duckduckgo.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1202552961248957/task/1213634576464400

### Description

The `MetricsPixelNumericValueDetector` lint rule was silently missing
violations when `MetricsPixel` was used in modules where the class is
loaded from a compiled dependency JAR — the constructor `PsiMethod`
can't be resolved in that case, so `visitConstructor` never fired.

This PR adds a fallback detection path via
`getApplicableUastTypes`/`createUastHandler` that triggers on all
`UCallExpression` nodes and uses source text matching + return type
filtering to identify `MetricsPixel` calls. Named-argument lookup was
also reworked to use `UNamedExpression` with a Kotlin PSI fallback,
making it robust regardless of argument order. All tests now skip
`TestMode.REORDER_ARGUMENTS` to suppress a known lint test
infrastructure warning caused by overlapping edits when positional args
are nested inside outer named args.

### Steps to test this PR

_MetricsPixelNumericValueDetector_
- [x] Change `SearchMetricPixelsPlugin` so one of them has a value
that's not a number, i.e. "test"
- [x] Run `./gradlew :feature-toggles-impl:lint`
- [x] Lint should throw an error

### UI changes
| Before  | After |
| ------ | ----- |
!(Upload before screenshot)|(Upload after screenshot)|


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Medium risk because it changes lint detection logic to rely on
UAST/PSI fallbacks and source-text matching, which could introduce false
positives/negatives across Kotlin call shapes.
> 
> **Overview**
> Fixes `MetricsPixelNumericValueDetector` missing violations when
`MetricsPixel` comes from a compiled dependency by adding a fallback
scan over all `UCallExpression`s and filtering to `MetricsPixel` calls
when the constructor can’t be resolved.
> 
> Reworks argument extraction to be robust to named/out-of-order
arguments by using `UNamedExpression` with a Kotlin PSI fallback, and
improves error location selection by reporting on the `value` expression
when possible.
> 
> Updates tests to reflect the real `MetricsPixel` signature (default
`type`), adds coverage for out-of-order named arguments, and skips
`TestMode.REORDER_ARGUMENTS` to avoid test infra issues.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
693bca6. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/72649045549333/task/1213641986703613?focus=true

### Description
Introduces new FF to enabled/disable pro entitlements fetcher
Decouples that logic from the actual feature

### Steps to test this PR

_Feature 1_
- [x] Apply staging patch from
https://app.asana.com/1/137249556945/project/1209991789468715/task/1210448620621729?focus=true
- [x] fresh install
- [x] Skip onboarding
- [x] Subscription settings are visible
- [x] Enter purchase flow -> ensure you see "See all Plans"
- [x] don't continue, navigate back
- [x] Go to feature flags inventory and disable `allowProTierPuchase`
- [x] Go back to settings and enter purchase flow -> should only allow
plus plans

### UI changes
| Before  | After |
| ------ | ----- |
!(Upload before screenshot)|(Upload after screenshot)|

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes subscription feature/entitlement fetching logic at app startup
to be controlled by a new remote flag, which could affect which base
plans are queried and cached. Impact is limited in scope but touches
subscription availability behavior.
> 
> **Overview**
> Decouples pro-tier entitlements refresh from purchase availability by
introducing a new `privacyPro.fetchProTierEntitlements` remote flag
(default **enabled**).
> 
> `SubscriptionFeaturesFetcher` now uses this new flag (instead of
`allowProTierPurchase`) to decide whether to include both Basic and
Advanced subscription products when selecting base plans to fetch and
cache subscription features/entitlements.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
5d1bab7. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/72649045549333/task/1213623825931245?focus=true

### Description
Add condition to show promo onboarding as soon as it is available in new
tab page

### Steps to test this PR
- [ ] Apply patch pinned on
https://app.asana.com/1/137249556945/project/1209991789468715/task/1210448620621729?focus=true

_Toggle visible_
- [x] Fresh install
- [x] Skip onboarding tapping on "I've been here before" > "Start
browsing"
- [x] Close/open the app until you see Subscription option on Settings
screen
- [x] Check duck ai toggle is enabled and visible
- [x] Background the app and set the date to 7 days from today.
- [x] Go back to app
- [x] Duck.ai toggle is visible with new tab page without the onboarding
promo dialog
- [x] Open a new tab
- [x] Check the onboarding dialog shows correctly

_Toggle no visible_
- [x] Fresh install
- [x] Skip onboarding tapping on "I've been here before" > "Start
browsing"
- [x] Close/open the app until you see Subscription option on Settings
screen
- [x] Check duck ai toggle is enabled but not visible (only full screen
new tab page)
- [x] Background the app and set the date to 7 days from today.
- [x] Go back to app
- [x] Check duck.ai toggle is not launched and the onboarding dialog
shows correctly

### No UI changes

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low-risk logic change that only gates the New Tab Page
onboarding-complete flag when the promo onboarding dialog is eligible to
show; main risk is unintended onboarding/promo dialog visibility
changes.
> 
> **Overview**
> Updates `BrowserTabViewModel.refreshCta` so
`isOnboardingCompleteInNewTabPage` is **false** while the promo
onboarding dialog is showing/eligible, allowing the New Tab Page to
display the promo onboarding UI.
> 
> Adds a unit test covering the scenario where the Privacy Pro promo CTA
is returned and the onboarding-complete flag must remain unset.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
a8442f9. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1202552961248957/task/1213647284656734

### Description

When the `uiLockChanged` JS callback fires, the `locked` value should be
forced to `false` if the current URL is from `duck.ai`. This is a
temporary fix to avoid the omnibar being hidden in some scenarios.

### Steps to test this PR

_Browser UI Lock — duck.ai behaviour_
- [ ] Open a duck.ai page and trigger a `uiLockChanged` JS callback with
`locked: true` — verify the UI lock does **not** activate
- [ ] Open a non-duck.ai page and trigger a `uiLockChanged` JS callback
with `locked: true` — verify the UI lock **does** activate
- [ ] Verify that disabling the `browserUiLock` feature flag prevents
the command from being issued entirely

|(Upload before screenshot)|(Upload after screenshot)|

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> <sup>[Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) is
generating a summary for commit
18e3f23. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
- Automated content scope scripts dependency update

This PR updates the content scope scripts dependency to the latest
available version and copies the necessary files.

Tests will only run if something has changed in the
`node_modules/@duckduckgo/content-scope-scripts` folder.

If only the package version has changed, there is no need to run the
tests.

If tests have failed, see
https://app.asana.com/0/1202561462274611/1203986899650836/f for further
information on what to do next.

_`content-scope-scripts` folder update_
- [x] All tests must pass
- [x] Privacy tests must pass

_Only `content-scope-scripts` package update_
- [ ] All tests must pass
- [ ] Privacy tests do not need to run

Co-authored-by: daxmobile <daxmobile@users.noreply.github.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/72649045549333/task/1213634152051167?focus=true

### Description
See attached task description

### Steps to test this PR
Smoke test PIR broker json download 

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes the broker-data download/extraction pipeline and adds ZIP path
validation; issues here could prevent broker updates or drop files
unexpectedly, but impact is contained to PIR updates.
> 
> **Overview**
> **Hardens PIR broker JSON updates** by downloading the broker ZIP to a
temp file in `cacheDir`, extracting only `.json` entries, and always
cleaning up temp/extract folders.
> 
> **Improves safety and robustness**: adds ZIP-slip protection via
canonical-path checks, reuses a single lazy Moshi `brokerAdapter`,
streams JSON parsing with okio instead of `readText()`, and ensures
coroutine cancellation is rethrown. Adds a unit test verifying malicious
ZIP entries are skipped and updates existing tests to provide
`cacheDir`.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
31d1c20. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/task/1213610121584461

### Description

When a user accepts Duck.ai terms and conditions and had already
accepted them previously, fire a pixel to track this duplicate
acceptance. Two separate pixels are sent depending on whether Sync is
enabled:
- `m_aichat_terms_accepted_duplicate_sync_on`
- `m_aichat_terms_accepted_duplicate_sync_off`

A new `DuckChatTermsOfServiceHandler` class encapsulates this logic,
keeping it out of the already-large `RealDuckChatJSHelper`. The
acceptance state is persisted in DataStore via a new
`DUCK_AI_TERMS_ACCEPTED` boolean key.

### Steps to test this PR

_Duplicate T&C acceptance pixel_
- [x] Open Duck.ai and accept terms and conditions
- [ ] Close and reopen Duck.ai, accept terms again
- [ ] Verify the appropriate pixel is fired (`sync_on` or `sync_off`
depending on Sync state)
- [ ] Verify first-time acceptance does not fire a pixel

### UI changes

None

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk: adds a new persisted boolean flag and additional pixel
firing paths without changing core chat or sync behavior.
> 
> **Overview**
> Adds a new `USER_DID_ACCEPT_TERMS_AND_CONDITIONS` report metric and JS
messaging method (`userDidAcceptTermsAndConditions`) to signal when the
user accepts Duck.ai terms.
> 
> On terms acceptance, `RealDuckChatPixels` now persists a
`DUCK_AI_TERMS_ACCEPTED` flag and fires a base acceptance pixel, plus an
additional **duplicate-acceptance** pixel when the flag was already set
(`DUCK_CHAT_TERMS_ACCEPTED_DUPLICATE_SYNC_ON` / `_SYNC_OFF` depending on
Sync state) via a new `DuckChatTermsOfServiceHandler` abstraction.
> 
> Extends datastore and unit tests to cover the new persistence and
pixel behavior.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
5fbdeb6. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Josh Leibstein <joshliebe@gmail.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1212015278241917/task/1211670072973939?focus=true

### Description

Create DaxPageHeader component in Compose for settings screens.

### Steps to test this PR

_Check DaxPageHeader_
- [ ] Open the application
- [ ] Go to the ADS screens from developer settings
- [ ] Open the "Templates" tab
- [ ] Check PageHeaders component follow our actual XML implementation

### UI changes
<img width="270" height="600" alt="image"
src="https://github.com/user-attachments/assets/01b4ea73-fafe-4721-a9cd-4ef47e946e38"
/>
Task/Issue URL: https://app.asana.com/1/137249556945/project/1198194956794324/task/1213651965660200?focus=true

### Description

Updates `url-predictor-android` from `0.3.13` to `0.3.15`. This version includes a fix for embedded newlines and tab characters in omnibar input being misclassified as a Navigate decision (triggering navigation) instead of a Search decision (triggering a search query).

### Steps to test this PR

_Test_
- [x] In toggle input screen, select duck.ai input
- [x] Input `https://example.com` + newline + `test` into the omnibar. It should open a DDG search, **not** navigate to example.com
- [x] Paste text with an embedded newline where the URL comes second, e.g. `hello world` on line 1 and `https://example.com` on line 2 — should open a DDG search, not navigate
- [ ] Paste text with a tab character followed by something URL-like, e.g. a tab then `example.com` — should open a DDG search, not navigate

_Regression — normal navigation still works_
- [x] Type a plain URL in the omnibar (e.g. `duckduckgo.com`) and confirm it navigates to the site
- [x] Type a full URL with scheme (e.g. `https://wikipedia.org`) and confirm it navigates directly
- [x] Type a search query (e.g. `how do penguins sleep`) and confirm it opens a DDG search results page

### UI changes

N/A — no UI changes.
Task/Issue URL: https://app.asana.com/1/137249556945/project/1211654189969294/task/1213651299034811

### Description

Adds a mic button on the duck.ai tab of the input screen that opens duck.ai directly in voice mode (`duck.ai/?mode=voice-mode`), giving users 1-click access to voice chat.

- New `duckAiVoiceEntryPoint` sub-feature flag (`DefaultFeatureValue.INTERNAL` — off in production, on in internal builds)
- New `openVoiceDuckChat()` on the `DuckChat` API → implemented in `RealDuckChat` by appending `mode=voice-mode` and forcing a new session
- `InputScreenViewModel`: extended the voice button visibility `combine` block to be tab-aware; duck.ai tab + flag on → button follows `voiceInputAllowed` (text presence) rather than `VoiceSearchAvailability`
- `InputScreenFragment`: both voice click handlers route to `viewModel.onVoiceEntryTapped()` on the duck.ai tab when flag is on
- Pixel: `m_aichat_voice_entry_tapped` fired on tap
- 7 new unit tests in `InputScreenViewModelTest`

### Steps to test this PR

_Enable flag_
- [x] In internal build, enable `duckAiVoiceEntryPoint` under duck.ai feature flags

_Duck.ai tab — empty field_
- [x] Open the input screen and switch to the duck.ai tab
- [x] Mic button is visible (even if private voice search is disabled in settings)
- [x] Tap the mic button → duck.ai opens with `?mode=voice-mode` in the URL, fresh session

_Duck.ai tab — with text_
- [x] Type something in the input field → mic button disappears, send button appears
- [x] Clear the field → mic button reappears

_Search tab (unchanged)_
- [x] Switch to the search tab → mic button follows voice search availability as before
- [x] Tap mic → system voice recognition launches (not duck.ai voice mode)

_Flag off_
- [x] Disable `duckAiVoiceEntryPoint` → duck.ai tab mic button behaves as before (follows voice search availability, launches system voice recognition)

### UI changes
| Before  | After |
| ------ | ----- |
|(Upload before screenshot)|(Upload after screenshot)|

---

> [!NOTE]
> **Medium Risk**
> Changes input-screen voice button behavior behind a new feature flag and adds a new DuckChat navigation path that forces a fresh session; risk is moderate due to UI/flow branching and URL/session handling.
> 
> **Overview**
> Adds a new, flag-gated *Duck.ai voice entry point* on the input screen: when on the Duck.ai tab and `duckAiVoiceEntryPoint` is enabled, the mic button opens Duck.ai with `mode=voice-mode` (forcing a new session) instead of launching system voice search.
> 
> Introduces `DuckChat.openVoiceDuckChat()` (implemented in `RealDuckChat` via `mode=voice-mode` query param), updates voice button visibility logic in `InputScreenViewModel` to be tab-aware, and wires both voice click handlers in `InputScreenFragment` to the new behavior. Adds new pixels (`m_aichat_voice_entry_tapped_*`) plus a shared `fireCountAndDaily` helper, and expands unit test coverage for the new routing/visibility/session semantics.
> 
> <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 0ed7e9a. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1208671518894266/task/1213642299611483?focus=true

### Description

Instead of skipping the workflow at the trigger level, the workflow now
always runs but gates all jobs behind a check_changes job that inspects
the diff.
- If only `.md` or `.github/` files changed → all jobs are skipped
(GitHub treats skipped as passing for branch protection)
- If `ci.yml` itself changed → all jobs run normally, so regressions are
caught before merging
- If any code file changed → all jobs run normally

### Steps to test this PR

QA optional:
- Open a PR that only modifies an `.md` file → check_changes should
pass, all other jobs should be skipped, PR should be mergeable
- Open a PR that only modifies a `.github` workflow file (not `ci.yml`)
→ check_changes should pass, all other jobs should be skipped, PR should
be mergeable
- Open a PR that modifies `.github/workflows/ci.yml` → all jobs should
run
- Open a PR that modifies any source file → all jobs should run

I tested all of these via
#7962.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Modifies the CI workflow execution logic to conditionally skip most
jobs based on diff contents, which could unintentionally reduce coverage
if the change-detection logic is wrong.
> 
> **Overview**
> The CI workflow no longer uses trigger-level `paths-ignore`; instead
it always starts and runs a new `check_changes` job that diffs the PR
(or always allows `push`/`workflow_dispatch`) to decide whether checks
should run.
> 
> All existing jobs (`code_formatting`, `unit_tests`, `lint`,
`android_tests`) now `need` `check_changes` and are gated by `if:
needs.check_changes.outputs.should_run == 'true'`, so docs-only or
non-`ci.yml` `.github/` changes skip the expensive checks while changes
to `ci.yml` or any code still run the full suite.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
9ff5ad8. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
#7955)

Task/Issue URL:
https://app.asana.com/1/137249556945/project/488551667048375/task/1213635828556477?focus=true

### Description
Pins the version of the gradler-profiler to v0.23.0 to stabilize our
metrics. Refs gradle/gradle-profiler#739.

### Steps to test this PR

No QA needed.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk CI workflow change that only affects the nightly benchmark
job by making the `gradleprofiler` install deterministic.
> 
> **Overview**
> Pins the GitHub Actions nightly build benchmark workflow to install
`gradleprofiler` version `0.23.0` via SDKMAN instead of the latest
version, to stabilize benchmark results.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
df240fc. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/488551667048375/task/1213690753964717?focus=true

### Description
Don't show subscription dialog for subscribers

### Steps to test this PR

_Pre steps_
- [x] Apply patch on
https://app.asana.com/1/137249556945/project/1209991789468715/task/1210448620621729?focus=true

_Returning users who skip onboarding_
- [x] Fresh install
- [x] Skip onboarding as a returning user (Skip onboarding as a
returning user (I've been here before > Start Browsing)
- [x] Go to Feature Flag Inventory and enable
`privacyProCtaSkippedOnboarding`
- [x] Purchase a test subscription
- [x] Close the app
- [x] Change the date in your device for >=7 days after today
- [x] Open the app and check subscription onboarding dialog doesn't
appear in a new tab page

_Regular onboarding_
- [x] Set the device time back to normal
- [x] Fresh install
- [x] Don't skip onboarding and go to browser
- [x] Purchase a test subscription
- [x] Go through onboarding and check Subscription onboarding dialog
doesn't appear after 'end onboarding dialog' in new tab page.

### No UI changes

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes onboarding CTA eligibility logic to depend on subscription
status, which can affect when onboarding is considered complete and
whether promo dialogs appear. Risk is moderate due to potential edge
cases if `SubscriptionStatus` is incorrect or delayed.
> 
> **Overview**
> Prevents the Privacy Pro onboarding CTA/dialog from showing to users
who already have an active subscription by additionally gating CTA
availability on `subscriptions.getSubscriptionStatus()` (only show when
status is `UNKNOWN`).
> 
> Updates onboarding completion/required CTA logic and adjusts/adds unit
tests to cover subscribed vs unsubscribed scenarios and to stub the new
subscription-status dependency.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
9e0b7dd. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
…t from the privacy-config module to the new one (#7967)

Task/Issue URL:
https://app.asana.com/1/137249556945/project/1211724162604201/task/1213642872484684?focus=true

### Description

Moved the `RequestBlocklist` interface and implementation from the
`privacy-config` module to a new dedicated `request-interception`
module. Updated the `RequestBlocklist.containedInBlocklist()` method
signature to accept `Uri` parameters instead of `String` parameters,
improving type safety and eliminating the need for string-based domain
extraction.

### Steps to test this PR

_Request Blocking Functionality_
- [x] Verify that request blocking still works correctly in the browser:
https://privacy-test-pages.site/privacy-protections/request-blocklist/
- [x] CI passes
- [x] com.duckduckgo.espresso.RequestBlocklistTest passes

### UI changes
No UI changes - internal refactoring only

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Medium risk because it relocates request-blocking logic into a new
module and changes `RequestBlocklist` to use `Uri` parameters, which
could alter matching behavior or wiring if any call sites weren’t
updated.
> 
> **Overview**
> Moves `RequestBlocklist` out of `privacy-config` into a new
`request-interception` API/impl module, and wires the app to depend on
it.
> 
> Updates the `containedInBlocklist` API (and call sites/tests) to
accept `Uri` instead of `String`, and adjusts
`WebViewRequestInterceptor` and the blocklist implementation to use
host-based matching (`baseHost`) rather than string domain extraction.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
680d0fe. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1200204095367872/task/1213720960821640?focus=true

### Description

- Catches `IllegalArgumentException` when `dismiss` is called on a
destroyed view (after a delay).

### Steps to test this PR

- [ ] Visit a site
- [ ] Tap the overflow menu and add a bookmark
- [ ] Verify that the confirmation dialog is dismissed after a delay

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk UI-only change that adds defensive error handling around
dialog dismissal to avoid a crash when the window is already gone.
> 
> **Overview**
> Prevents a crash in `BookmarkAddedConfirmationDialog` when the
auto-dismiss timer fires after the dialog window has already been
removed.
> 
> The auto-dismiss `dismiss()` call is now wrapped in a `try/catch` for
`IllegalArgumentException` and logs a message instead of throwing.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
a14cdad. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1202552961248957/task/1213449998545088?focus=true

### Description

Removes the temporary Block Store de-risking capability observer now
that sufficient production data has been collected.

- Deleted `SyncAutoRecoveryCapabilityObserver` and its tests
- Removed 9 Block Store daily pixels from `SyncPixels.kt` and
`SyncPixelParamRemovalPlugin.kt`
- Removed `syncAutoRecoveryCapabilityDetectionWrite` and
`syncAutoRecoveryCapabilityDetectionRead` feature flags from
`SyncFeature.kt`

### Steps to test this PR
**QA optional**

- [ ] [Optional] Build and verify app launches without crashes
- [ ] [Optional] Confirm no Block Store capability pixels are fired


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Low risk cleanup that removes de-risking telemetry and its feature
flags/tests; behavior change is limited to no longer performing Block
Store capability checks or emitting related daily pixels.
> 
> **Overview**
> Removes the temporary Block Store de-risking path by deleting
`SyncAutoRecoveryCapabilityObserver` (and its test) that ran capability
read/write checks after privacy config downloads.
> 
> Cleans up associated telemetry by dropping the Block Store daily pixel
definitions and their parameter-removal entries, and removes the two
remote feature toggles
(`syncAutoRecoveryCapabilityDetectionWrite`/`Read`) used to gate this
logic.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
0b5823f. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Craig Russell <1336281+CDRussell@users.noreply.github.com>
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1211724162604201/task/1213684338936124?focus=true

### Description

Added a new lint rule that prevents the use of `postValue()` on
`SingleLiveEvent` instances. The rule enforces the use of `setValue()`
instead to avoid silently dropping commands when multiple `postValue()`
calls occur before the main thread processes them.

The detector identifies calls to `postValue()` on `SingleLiveEvent` or
its subclasses and reports an error with guidance to use `setValue()` on
the main thread or wrap background thread calls with
`withContext(dispatchers.main())`.

### Steps to test this PR

_Lint Rule Validation_
- [ ] Create a class with a `SingleLiveEvent` property and call
`postValue()` on it - verify lint error appears
- [ ] Change the same call to use `setValue()` or `.value = ...` -
verify lint error disappears
- [ ] Call `postValue()` on a regular `MutableLiveData` instance -
verify no lint error occurs

### UI changes
No UI changes. 

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Medium risk because it changes how several UI `command` emissions are
dispatched (switching to main-thread `setValue`), which can affect
timing/order of one-shot navigation/dialog events if any callers relied
on background posting.
> 
> **Overview**
> Adds a new lint rule (`NoPostValueOnSingleLiveEventDetector`)
registered in the project’s lint registry (with unit tests) to **error**
on `SingleLiveEvent.postValue()` usage, guiding developers to use
main-thread `setValue`/`.value = ...` to avoid dropped commands.
> 
> Updates multiple ViewModels (notably `BrowserTabViewModel`, plus
`FeedbackViewModel` and `BookmarksViewModel`) to replace `postValue`
with direct `.value` assignments, and wraps previously background-thread
emissions in `dispatchers.main()` launches where needed.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
bc1b973. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL: https://app.asana.com/1/137249556945/project/72649045549333/task/1211766481496464?focus=true

### Description
Adds in the headless sync setup using the persisted sync recovery key. 

This is still gated by the `syncAutoRestore` feature flag which **remains disabled** for now (you'll need to update this manually for some of the tests)

### Steps to test this PR

**Notes**
- Prerequisites: Internal build installed on a device with Google Play Services, logged into Google account. 
- Navigate to Settings > Sync Dev Settings to access Block Store controls.                                                                                          
- ℹ️ Clearing data and relaunching the app does not restore Block Store data; it has to be an uninstall/reinstall.
- Suggested logcat filter: `Sync-Recovery|Sync-AutoRestore`
                                                            
**Scenario 1: Feature flag OFF (default) — no restore offered regardless of stored key**
  - [x] Fresh install
  - [x] Launch app and verify the "Restore" dialog is not shown, normal onboarding flow proceeds
  - [x] go to Sync Dev Settings and write any string to Block Store
  - [x] Uninstall and reinstall (do not clear app data)
  - [x] Launch app and verify the "Restore" dialog is still not shown (because flag is off)

**Scenario 2: Feature flag ON, no recovery key — no restore offered**
  - [x] Apply the patch defined below to hardcode `syncAutoRestore` to be enabled, and install
  - [x] Clear app data to ensure Block Store has no data
  - [x] Launch app — verify the "Restore" dialog is not shown, normal onboarding proceeds

**Scenario 3: Feature flag ON, recovery key present — user accepts restore**
  - [x] Keep the hardcoded FF enabled changes from before
  - [x] Save a password or two
  - [x] Set up Sync on the device, using Sync & Backup -> Sync & Back Up This Device and copy the recovery key when it's available using the `Copy code` button
  - [x] Paste the recovery key into Block Store via Sync Dev Settings and use the `Write` button to persist it
  - [x] Uninstall and reinstall (do not clear app data)
  - [x] Go through onboarding — verify the "Restore" dialog is shown
  - [x] Tap "Restore My Stuff" — verify onboarding continues normally (e.g., comparison chart shown next)
  - [x] Complete onboarding, then go to Settings > Sync — verify sync account is re-established
  - [x] Verify previous password is available

**Scenario 4: Feature flag ON, recovery key present — user skips restore**
  - [x] Repeat setup from Scenario 3 (to get a valid recovery code in Block Store, uninstall and then reinstall)
  - [x] Launch app — verify the "Restore" dialog is shown
  - [x] Tap "Skip" button from that dialog — verify you see the `Got it! I'll skip other tips` dialog
  - [x] Complete onboarding, then go to Settings > Sync — verify sync is not set up

**Scenario 5: Invalid code persisted**
  - [x] Use `Sync Dev Settings` to write an invalid recovery code (e.g., a few random characters)
  - [x] Uninstall and reinstall (do not clear app data)
  - [x] Launch app — verify the "Restore" dialog is shown
  - [x] Tap `Restore My Stuff`. Verify the UX continues normally and in logs you see `Sync-Recovery: restore failed`

## Patch to enable FF for `syncAutoRestore`
```
Index: sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt
--- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt	(revision Staged)
+++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt	(date 1773231063963)
@@ -78,6 +78,6 @@
     @Toggle.DefaultValue(DefaultFeatureValue.TRUE)
     fun syncAutoRecoveryCapabilityDetectionRead(): Toggle
 
-    @Toggle.DefaultValue(DefaultFeatureValue.FALSE)
+    @Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
     fun syncAutoRestore(): Toggle
 }
```
Task/Issue URL: https://app.asana.com/1/137249556945/project/72649045549333/task/1213646602671393?focus=true

### Description

When auto-restoring a sync account from Block Store on reinstall, the previous implementation called `login()` without providing the original device ID, causing the sync backend to assign a new device ID. This left the old device entry as an orphan on the account — visible to other connected devices as a ghost entry.

This PR fixes the orphaned device problem by storing the device ID alongside the recovery code in Block Store as a JSON payload, then reusing that device ID during auto-restore login. This matches the approach iOS already uses.

No user-facing prod changes as it is still guarded by `syncAutoRestore` FF which remains `DISABLED`

### Steps to test this PR

Logcat filter `message~:"Sync-Recovery|Sync-AutoRestore"`
**Pre-requsites**
1. Device/emulator with Google Play Services
2. Be signed in to the Google account on your device, and have device-level backups enabled
3. Have device-level auth set (PIN/Pattern/Password)

### Feature flag disabled (default)
- [x] Fresh install `internalDebug`, launch app
- [x] Verify in logs, `Sync-AutoRestore: canRestore=false`

### Feature flag enabled
❗ **hardcode the feature flag to enabled for the following tests**

**Testing recovery code cleared when logging out of sync**
- [x] Apply patch below
- [x] Install `internalDebug` and launch
- [x] Verify `canRestore=false` in logs
- [x] Verify you do **not** see "Restore my stuff" dialog
- [x] Set up sync `Sync and Back Up This Device` then disable sync again
- [x] Verify in logs, `sync disabled, clearing recovery code from Block Store`

**Ensuring device not orphaned on restore**
- [x] Set up sync again, and this time copy the recovery code using the `Copy Code` button
- [x] Visit `Sync Dev Settings`, and paste into the `Recovery code` edit text (in the `Persistent storage` section)
- [x] Scroll down to the `Account Settings` section, and tap on `Device id`'s value
- [x] Paste into the `Device ID` edit text
- [x] Tap the `Write` button and verify you see the `Stored successfully` toast and JSON containing both recovery and device ID is shown
- [x] Uninstall and reinstall (do not use clear app data). Launch app
- [x] Verify you see in logs, `canRestore=true`
- [x] Verify you are offered to `Restore My Stuff`. Tap that button.
- [x] Skip rest of onboarding, and visit `Settings -> Sync & Backup`
- [x] Verify sync is set up, and that there is only one device showing


### Patch to enabled `syncAutoRestore` feature flag
```
Index: sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt
--- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt	(revision Staged)
+++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt	(date 1773659914685)
@@ -78,6 +78,6 @@
     @Toggle.DefaultValue(DefaultFeatureValue.TRUE)
     fun syncAutoRecoveryCapabilityDetectionRead(): Toggle
 
-    @Toggle.DefaultValue(DefaultFeatureValue.FALSE)
+    @Toggle.DefaultValue(DefaultFeatureValue.INTERNAL)
     fun syncAutoRestore(): Toggle
 }
```

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes sync auto-restore login behavior to reuse a stored device ID and adds new persistence/cleanup logic; mistakes could cause failed restores or unexpected sign-in/device registration behavior.
> 
> **Overview**
> Fixes sync auto-restore creating *orphaned/ghost devices* by allowing `processCode`/recovery `login` to accept an `existingDeviceId` and using it during restore instead of always generating a new one.
> 
> Introduces `SyncAutoRestoreManager` to persist a JSON payload (`recovery_code` + optional `device_id`) in Block Store, updates `RealSyncAutoRestore` to read that payload (and to hard-skip when the `syncAutoRestore` flag is off), and adds a lifecycle observer that clears the stored recovery payload when the user signs out while auto-restore is enabled.
> 
> Updates internal sync settings UI to write/read the new payload format (separate recovery-code and device-id inputs, plus tap-to-copy fields) and adjusts/extends unit tests to cover the new manager, observer, and updated auto-restore flow.
> 
> <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit fcb4307. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/488551667048375/task/1213614217074373?focus=true

### Description
Enables Develocity PTS for JVM unit tests for local builds and PR
checks. Keeps it disabled for post-merge checks and nightly flows since
we want to run the full suite.

This runs with the [Standard
profile](https://docs.gradle.com/develocity/current/using-develocity/predictive-test-selection/#selection-profiles)
that balances speed and selecting relevant tests. Alternatively, we can
go with Conservative to select more tests but reduces time savings.

### Steps to test this PR

_Run tests locally_
- [x] Run `./gradlew :pir-impl:testDebugUnitTest -Dpts.enabled=false`
(replace `pir-impl` with any module you are familiar with the unit
tests) to get a baseline time of how long it takes. It should run all
tests in that module
- [x] Run `./gradlew :pir-impl:testDebugUnitTest` twice in a row. The
second time it should finish in less than a second and run no tests.
- [ ] Now change some code in that module in a way that would break the
test. For example in `pir-impl`, edit `PirAuthInterceptor:65` and change
`bearer` to `Bearer`. This change will break one of the tests that
checks for correct header value.
- [ ] Run `./gradlew :pir-impl:testDebugUnitTest` again. You should see
something like `Predictive Test Selection: 4 of 74 test classes selected
with profile 'Standard' (saving 54.371s serial time)` and the tests that
were run should fail.

### UI changes
No UI change

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes the JVM unit-test execution model in CI by enabling Develocity
Predictive Test Selection by default, which can unintentionally skip
relevant tests if misconfigured. Adds JUnit Platform/Vintage
dependencies across modules, which may change how tests are discovered
and run.
> 
> **Overview**
> **Enables Develocity Predictive Test Selection (PTS) for JVM unit
tests** by configuring all Gradle `Test` tasks to `useJUnitPlatform()`
and wiring `develocity.predictiveTestSelection.enabled` to a
`-Dpts.enabled` system property (defaulting to `true`).
> 
> CI workflows now **disable PTS for post-merge/nightly and external
reference test runs** by passing `-Dpts.enabled=false`, while PR checks
keep PTS enabled by default. This also adds
`org.junit.vintage:junit-vintage-engine` (and its version pin) to test
dependencies so existing JUnit 4 tests continue to run under the JUnit
Platform.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
e0dfd38. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/488551667048375/task/1213634152051160?focus=true

### Description
See attached description

### Steps to test this PR
https://app.asana.com/1/137249556945/task/1213721083788440?focus=true

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Adds a large set of new bundled broker configuration JSONs that
directly drive PIR scan/opt-out automation; incorrect selectors/flows or
`removedAt` flags could cause broken runs or unintended broker
enablement/disablement.
> 
> **Overview**
> Adds a batch of new broker JSON assets under
`pir-impl/src/main/assets/brokers/`, expanding the set of bundled broker
definitions used for PIR scanning and opt-out flows.
> 
> These configs include new `scan`/`optOut` step recipes (including
captcha handling and email confirmations), parent/mirror-site
relationships, scheduling parameters, and per-broker activation state
via `removedAt` (some brokers ship pre-removed).
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
5b9dc42. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1200204095367872/task/1213433888294716?focus=true

### Description

- Adds the native input to the Duck.ai contextual sheet

### Steps to test this PR

- [ ] Enable the native input
- [ ] Open contextual Duck.ai and send prompt
- [ ] Verify that the native input is visible
- [ ] Send a prompt
- [ ] Verify that the prompt is submitted
- [ ] Change to search
- [ ] Submit a query
- [ ] Very that contextual is closed and the search is submitted

### UI changes
| Before  | After |
| ------ | ----- |
<img width="1280" height="2856" alt="Screenshot_20260317_004850"
src="https://github.com/user-attachments/assets/e20c0ffc-de95-4ea8-98e3-36b65285c59c"
/>|<img width="1280" height="2856" alt="Screenshot_20260317_010044"
src="https://github.com/user-attachments/assets/2682325c-72b6-4617-8668-bbe9a6e0ad44"
/>


<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Introduces a new native input overlay that sends JS subscription
events into the contextual WebView and changes sheet-mode
rendering/visibility, so regressions could impact prompt submission or
UI state toggling.
> 
> **Overview**
> Adds an optional **native input overlay** to the Duck.ai contextual
sheet when in WebView mode, driven by a new
`ContextualNativeInputManager` that wires up `NativeInputModeWidget` and
toggles visibility based on the user setting.
> 
> Native chat prompts are now submitted via `JsMessaging` subscription
events (`submitAIChatNativePrompt` / `submitPromptInterruption`), while
search submissions close the contextual sheet and open the query in a
new browser tab. The layout is updated to wrap the `WebView` in a
container and overlay the new input card at the bottom.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
31a9ea3. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/488551667048375/task/1213310129933666?focus=true

### Description

- Adds a Duck.ai dev setting to override the Duck.ai URL
- Centralizes all uses of the “duck.ai” domain
- Changes the default `DUCK_CHAT_WEB_LINK` to
"https://duck.ai/chat?duckai=5” (the same URL that’s used in the config)

### Steps to test this PR

- [ ] Go to Settings > Developer Settings > Custom Duck.ai URL
- [ ] Enter a custom URL
- [ ] Tap “Save"
- [ ] Verify that the app is restarted
- [ ] Go to Duck.ai
- [ ] Verify that the custom URL is used

### UI changes
| Developer Settings  | Custom Duck.ai URL |
| ------ | ----- |
<img width="1080" height="2340" alt="Screenshot_20260312_212642"
src="https://github.com/user-attachments/assets/fff528da-6a7d-40ef-8fbc-639c71283547"
/>|<img width="1080" height="2340" alt="Screenshot_20260312_213453"
src="https://github.com/user-attachments/assets/32ff1142-a813-4544-9db3-41d287f31730"
/>





<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches multiple privacy/cookie-clearing and JS messaging allowlist
paths by replacing hardcoded `duck.ai` checks with an injected host
provider, so a misconfiguration could break Duck.ai navigation, data
clearing, or messaging on that domain.
> 
> **Overview**
> Adds a `DuckAiHostProvider` abstraction and rewires Duck.ai-related
logic to use it instead of hardcoded `duck.ai` (cookies/third‑party
cookie exceptions, IndexedDB/site data clearing exclusions, site
permissions microphone recovery allowlist, and multiple JS messaging
`allowedDomains` lists).
> 
> Introduces an *internal/dev-only* `duckchat-internal` module and
Developer Settings UI to override the Duck.ai URL, persisting it in a
small data store and replacing the default provider via DI; the DuckChat
entry URL is also updated to the direct `https://duck.ai/chat?duckai=5`
form.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
4a3a624. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Task/Issue URL:
https://app.asana.com/1/137249556945/project/1174433894299346/task/1213630582405252

### Description

Adds a `FirstScreenHandlerImpl` that decides the first screen shown when
the app is opened, with two modes controlled by the
`showNTPAfterIdleReturn` feature flag:

**When `showNTPAfterIdleReturn` is enabled** (new behavior):
- On every app open (fresh launch or return from background), checks if
the idle timeout has elapsed since the app was last backgrounded.
- If the timeout has passed (or no previous timestamp exists), delegates
to `ShowOnAppLaunchOptionHandler` to show the configured launch screen.
- If the timeout hasn't passed, does nothing (user returns to their
previous state).
- Timeout is configurable via remote settings JSON (`timeoutMinutes`
field), defaults to 30 minutes.

**When `showNTPAfterIdleReturn` is disabled** (legacy behavior):
- Only on fresh launches: delegates to `ShowOnAppLaunchOptionHandler` if
`showOnAppLaunchFeature` is enabled.
- Non-fresh launches (return from background): does nothing.

Key changes:
- New `FirstScreenHandlerImpl` registered as a
`BrowserLifecycleObserver` via `@ContributesMultibinding`
- `showNTPAfterIdleReturn` is checked first and takes precedence over
`showOnAppLaunchFeature`
- Records background timestamp on `onClose()` using
`System.currentTimeMillis()` (survives reboots)
- `BrowserViewModel` no longer owns show-on-app-launch logic — fully
decoupled
- New `lastSessionBackgroundTimestamp` in `SettingsDataStore` (separate
from `AutomaticDataClearer`'s timestamp)
- `showNTPAfterIdleReturn` feature flag defaults to `FALSE`, enabled on
internal builds via `@InternalAlwaysEnabled`

### Steps to test this PR

_Idle return enabled — timeout exceeded (cold start)_
- [x] Enable `showNTPAfterIdleReturn` feature flag (auto-enabled on
internal builds)
- [x] Open the app and navigate to a website
- [x] Background the app and wait longer than the configured timeout
(default 1 min on internal)
- [x] Reopen the app — the configured ShowOnAppLaunch option should be
applied

_Idle return enabled — timeout exceeded (fresh launch)_
- [x] Enable `showNTPAfterIdleReturn` feature flag
- [x] Open the app and navigate to a website
- [x] Force-stop the app and wait longer than the configured timeout
- [x] Reopen the app — the configured ShowOnAppLaunch option should be
applied

_Idle return enabled — timeout not exceeded_
- [x] Enable `showNTPAfterIdleReturn` feature flag
- [x] Open the app and navigate to a website
- [x] Background the app and reopen within the timeout window
- [x] The previous tab should still be visible (no action taken)

_Idle return enabled — first ever launch_
- [ ] Enable `showNTPAfterIdleReturn` feature flag
- [ ] Clear app data or fresh install
- [ ] Open the app — the configured ShowOnAppLaunch option should be
applied (no previous timestamp)

_Idle return disabled — fresh launch (legacy behavior)_
- [x] Disable `showNTPAfterIdleReturn` feature flag
- [x] Enable `showOnAppLaunchFeature` and set the option to "New Tab
Page"
- [x] Force-stop the app and reopen — a new tab page should be shown
- [x] Set the option to "Specific Page" with a URL, force-stop and
reopen — the specific page should load
- [x] Set the option to "Last Opened Tab", force-stop and reopen — the
last opened tab should be shown

_Idle return disabled — non-fresh launch (legacy behavior)_
- [x] Disable `showNTPAfterIdleReturn` feature flag
- [x] Open the app, navigate to a website, background it for any
duration
- [x] Reopen the app — the previous tab should still be visible (no
action taken)

_Idle return disabled — ShowOnAppLaunch also disabled_
- [x] Disable both `showNTPAfterIdleReturn` and `showOnAppLaunchFeature`
- [x] Force-stop and reopen the app — default behavior, no delegation

### UI changes

No UI changes — this is a behavioral change only.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes app-open/return behavior by moving “show on app launch”
decisions into a new lifecycle observer and gating it behind remote
config + persisted timestamps, which could affect what users see when
resuming the app.
> 
> **Overview**
> Adds `FirstScreenHandlerImpl` as a `BrowserLifecycleObserver` to
centralize first-screen selection on app open. When remote flag
`androidBrowserConfig.showNTPAfterIdleReturn` is enabled, it
conditionally applies `ShowOnAppLaunchOptionHandler` based on elapsed
time since the app was last backgrounded (remote-configurable
`timeoutMinutes`, default 30m); otherwise it preserves the legacy
*fresh-launch only* behavior.
> 
> Removes the previous “show on app launch” handling from
`BrowserActivity`/`BrowserViewModel`, introduces persisted
`SettingsDataStore.lastSessionBackgroundTimestamp`, adds the new remote
sub-toggle to `AndroidBrowserConfigFeature`, and updates/adds tests
accordingly.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
2d06fa7. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.